﻿//     Copyright (c) Microsoft Corporation.  All rights reserved.

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Gateway.Utils;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Gateway.Console.Services.Azure
{
    public class AadGraphApi : IDisposable
    {
        const string RDFE_AppId                     = "797f4846-ba00-4fd7-ba43-dac1f8f63013";

        static readonly string AppPrincipalId       = Configuration.ClientID;

        private GraphApiRestClient _restClient;
        private AuthenticationResult _accessToken;

        public AadGraphApi(string tenant, AuthenticationResult accessToken)
        {
            _restClient = new GraphApiRestClient(tenant, accessToken);
            _accessToken = accessToken;
        }

        public void Dispose()
        {
            _restClient.Dispose();
        }

        public async Task<bool> IsRdfeAccessEnabledAsync()
        {
            try
            {
                var servicePrincipals = new GraphApiEntityCollection(this._restClient, "servicePrincipals");
                var clientPrincipal = await servicePrincipals.FindObjectAsync("appId", AppPrincipalId);
                var rdfeApiPrincipal = await servicePrincipals.FindObjectAsync("appId", RDFE_AppId);
                if (clientPrincipal != null && rdfeApiPrincipal != null)
                {
                    string clientObjectId = clientPrincipal.Value<string>("objectId");
                    string rdfeObjectId = rdfeApiPrincipal.Value<string>("objectId");
                    return await GraphApiEntityCollection.FindObjectAsync(this._restClient, "permissions", 
                        (permission) => clientObjectId.Equals(permission.Value<string>("clientId"), StringComparison.OrdinalIgnoreCase) &&
                                        rdfeObjectId.Equals(permission.Value<string>("resourceId"), StringComparison.OrdinalIgnoreCase)) != null;
                }
            }
            catch (GraphApiException ex)
            {
                if (ex.ErrorCode == "Authorization_RequestDenied")
                {
                    // Non-admin users can't read the permissions entityset, so we have to presume that it is enabled
                    return true;
                }

            }
            return false;
        }

        public async Task EnableRdfeAccessAsync()
        {
            var servicePrincipals = new GraphApiEntityCollection(this._restClient, "servicePrincipals");
            var clientPrincipal = await servicePrincipals.FindObjectAsync("appId", AppPrincipalId);
            var rdfeApiPrincipal = await servicePrincipals.FindObjectAsync("appId", RDFE_AppId);
            if (clientPrincipal != null && rdfeApiPrincipal != null)
            {
                // Some AAD tenants don't have the correct SP Names & aren't being back-filled. 
                // ie. they're missing https://management.core.windows.net/ from the name, which RDFE requires to be the audience
                var rdfeNames = rdfeApiPrincipal.Value<JArray>("servicePrincipalNames");
                if (!rdfeNames.Any(name => String.Equals(AzureEnvironment.RdfeManagementEndpoint, (string)((JValue)name).Value, StringComparison.OrdinalIgnoreCase)))
                {
                    rdfeNames.Add(AzureEnvironment.RdfeManagementEndpoint);
                    await _restClient.MergeJObjectAsync("servicePrincipals", rdfeApiPrincipal.Value<string>("objectId"), rdfeApiPrincipal);
                }

                await _restClient.PostObjectAsync("permissions", new
                {
                    clientId = clientPrincipal.Value<string>("objectId"),
                    resourceId = rdfeApiPrincipal.Value<string>("objectId"),
                    expiryTime = DateTime.MaxValue,
                    scope = "user_impersonation",
                    consentType = "AllPrincipals",
                });
            }
        }

        public async Task<string> GetDefaultDomainNameAsync()
        {
            var tenantInfo = await GraphApiEntityCollection.FindObjectAsync(this._restClient, "tenantDetails", item => true);
            if (tenantInfo != null)
            {
                var defaultDomain = ((JArray)tenantInfo["verifiedDomains"]).FirstOrDefault(domain => domain.Value<bool>("default"));
                if (defaultDomain != null)
                {
                    return defaultDomain.Value<string>("name");
                }
            }
            return this._accessToken.TenantId;
        }

        public async Task EnableWebSSOAsync(string appIdUri, bool multiTenantAccess, string replyUrl)
        {
            var application = await GraphApiEntityCollection.FindObjectAsync(this._restClient, "applications",
                (app) => ((JArray)app["identifierUris"]).Any(uri => String.Equals(uri.Value<string>(), appIdUri, StringComparison.OrdinalIgnoreCase)));
            if (application != null)
            {
                // Verify that the reply address is correct
                var replyUrls = (JArray)application["replyUrls"];
                if (!replyUrls.Any(reply => String.Equals(replyUrl, reply.Value<string>(), StringComparison.OrdinalIgnoreCase)))
                {
                    replyUrls.Add(replyUrl);
                    await this._restClient.MergeJObjectAsync("applications", application.Value<string>("objectId"), application);
                }
            }
            else
            {
                application = await this._restClient.PostObjectAsync("applications",
                    new
                    {
                        displayName = String.Format("Gateway at {0}", replyUrl),
                        availableToOtherTenants = multiTenantAccess,
                        homepage = replyUrl,
                        replyUrls = new string[] { replyUrl },
                        identifierUris = new string[] { appIdUri },
                    });
            }
            string appId = application.Value<string>("appId");

            var servicePrincipals = new GraphApiEntityCollection(this._restClient, "servicePrincipals", String.Format("appId eq Guid'{0}'", appId));
            var servicePrincipal = (await servicePrincipals.GetObjectsAsync()).FirstOrDefault();
            if (servicePrincipal == null)
            {
                await this._restClient.PostObjectAsync("servicePrincipals",
                    new
                    {
                        accountEnabled = true,
                        appId = appId,
                    });
            }
        }

        class GraphApiEntityCollection
        {
            GraphApiRestClient _restClient;
            string _entityName;
            string _filterPredicate;
            IList<GraphApiResponsePage> _responsePages;

            public GraphApiEntityCollection(GraphApiRestClient restClient, string entityName, string filterPredicate = "")
            {
                this._restClient = restClient;
                this._entityName = entityName;
                this._filterPredicate = filterPredicate;
                this._responsePages = new List<GraphApiResponsePage>();
            }

            public static async Task<JObject> FindObjectAsync(GraphApiRestClient restClient, string entityName, string matchAttribute, string matchValue)
            {
                return await FindObjectAsync(restClient, entityName, item => IsObjectMatch(item, matchAttribute, matchValue));
            }

            public static async Task<JObject> FindObjectAsync(GraphApiRestClient restClient, string entityName, Func<JObject, bool> predicate)
            {
                var entities = new GraphApiEntityCollection(restClient, entityName);
                return await entities.FindObjectAsync(predicate);
            }

            public async Task<JObject> FindObjectAsync(string matchAttribute, string matchValue)
            {
                return await FindObjectAsync(item => IsObjectMatch(item, matchAttribute, matchValue));
            }

            public async Task<JObject> FindObjectAsync(Func<JObject, bool> predicate)
            {
                JObject foundItem = (JObject)this._responsePages
                    .SelectMany(page => page.Values)
                    .FirstOrDefault(token => predicate((JObject)token));
                if (foundItem != null)
                {
                    return foundItem;
                }
                return await EnumerateObjects(predicate);
            }

            public async Task<IEnumerable<JObject>> GetObjectsAsync()
            {
                await EnumerateObjects(_ => false);
                return this._responsePages
                    .SelectMany(page => page.Values.Cast<JObject>());
            }

            private async Task<JObject> EnumerateObjects(Func<JObject, bool> predicate)
            {
                for (var responsePage = await GetNextPage(this._responsePages.LastOrDefault());
                    responsePage != null;
                    responsePage = await GetNextPage(responsePage))
                {
                    this._responsePages.Add(responsePage);
                    var foundItem = (JObject)responsePage.Values.FirstOrDefault(token => predicate((JObject)token));
                    if (foundItem != null)
                    {
                        return foundItem;
                    }
                }
                return null;
            }

            private async Task<GraphApiResponsePage> GetNextPage(GraphApiResponsePage previousPage)
            {
                if (previousPage != null)
                {
                    return await this._restClient.GetNextResultsPageAsync(previousPage, this._entityName, this._filterPredicate);
                }
                else
                {
                    return await this._restClient.GetEntityCollectionAsync(this._entityName, this._filterPredicate);
                }
            }

            private static bool IsObjectMatch(JObject item, string matchAttribute, string matchValue)
            {
                return matchValue.Equals(item.Value<string>(matchAttribute), StringComparison.OrdinalIgnoreCase);
            }
        }

        class GraphApiResponsePage
        {
            public JArray Values { get; set; }
            public string NextPageToken { get; set; }

            public bool HasNextPage
            {
                get { return !String.IsNullOrWhiteSpace(this.NextPageToken); }
            }
        }

        class GraphApiException : ApplicationException
        {
            public GraphApiException(string message, string code, string errorMessage)
                : base(FormatMessage(message, code, errorMessage))
            {
                this.ErrorCode = code;
                this.ErrorMessage = errorMessage;
            }

            static string FormatMessage(string message, string code, string errorMessage)
            {
                return String.Format("{0} Details: [{1}], {2}", message, code, errorMessage);
            }

            public string ErrorCode { get; set; }
            public string ErrorMessage { get; set; }
        }

        class GraphApiRestClient : IDisposable
        {
            private HttpClient _httpClient;
            private string _tenant;

            const string SkipToken      = "$skiptoken";

            public GraphApiRestClient(string tenantName, AuthenticationResult accessToken)
            {
                _httpClient = new HttpClient();
                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(accessToken.AccessTokenType, accessToken.AccessToken);
                _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("api-version", "1.0");
                var acceptHeader = new MediaTypeWithQualityHeaderValue("application/json");
                acceptHeader.Parameters.Add(new NameValueHeaderValue("odata", "nometadata"));
                _httpClient.DefaultRequestHeaders.Accept.Add(acceptHeader);
                _tenant = tenantName;
            }

            public void Dispose()
            {
                _httpClient.Dispose();
            }

            private string GetEntityUri(string entityName, params string[] additionalSegments)
            {
                string endpoint = AzureEnvironment.GraphApiEndpoint;
                if (endpoint[endpoint.Length - 1] == '/')
                {
                    endpoint = endpoint.Substring(0, endpoint.Length - 1);
                }
                return String.Join("/", new string[] { endpoint, this._tenant, entityName }
                                            .Concat(additionalSegments ?? Enumerable.Empty<string>()));
            }

            private string GetEntityQueryUri(string entityName, string filterPredicate)
            {
                string entityUri = GetEntityUri(entityName);
                if (!String.IsNullOrWhiteSpace(filterPredicate))
                {
                    return entityUri + "?$filter=" + filterPredicate;
                }
                return entityUri;
            }

            public async Task<GraphApiResponsePage> GetEntityCollectionAsync(string entityName, string filterPredicate = "")
            {
                return await GetResponsePageAsync(GetEntityQueryUri(entityName, filterPredicate), entityName);
            }

            public async Task<GraphApiResponsePage> GetNextResultsPageAsync(GraphApiResponsePage previousResponse, string entityName, string filterPredicate = "")
            {
                if (previousResponse.HasNextPage)
                {
                    var uri = new UriBuilder(GetEntityQueryUri(entityName, filterPredicate));
                    string skipTokenParam = String.Format("{0}={1}", SkipToken, previousResponse.NextPageToken);
                    if (uri.Query != null && uri.Query.Length > 1)
                    {
                        uri.Query = uri.Query.Substring(1) + "&" + skipTokenParam;
                    }
                    else
                    {
                        uri.Query = skipTokenParam;
                    }
                    return await GetResponsePageAsync(uri.ToString(), entityName);
                }
                return null;
            }

            private async Task<GraphApiResponsePage> GetResponsePageAsync(string graphUrl, string entityName)
            {
                using (var response = await _httpClient.GetAsync(graphUrl))
                {
                    var reader = new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync()));
                    var jsonResponse = JObject.Load(reader);
                    if (response.IsSuccessStatusCode)
                    {
                        return new GraphApiResponsePage
                        {
                            Values = jsonResponse["value"] as JArray,
                            NextPageToken = ExtractSkipToken(jsonResponse.Value<string>("odata.nextLink")),
                        };
                    }
                    throw CreateGraphException(String.Format("Failure reading entities: {0}", entityName), jsonResponse);
                }
            }

            private string ExtractSkipToken(string nextLink)
            {
                if (String.IsNullOrWhiteSpace(nextLink))
                {
                    return String.Empty;
                }
                var uri = new UriBuilder(nextLink);
                if (String.IsNullOrWhiteSpace(uri.Query))
                {
                    return String.Empty;
                }
                var query = HttpUtility.ParseQueryString(uri.Query.Substring(1));
                return query[SkipToken];
            }

            public async Task<JObject> PostObjectAsync<T>(string entityName, T contentObject)
            {
                using (var response = await _httpClient.PostAsJsonAsync(GetEntityUri(entityName), contentObject))
                {
                    await ProcessResponseAsync(response, "Failed to POST new object.");
                    var reader = new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync()));
                    return JObject.Load(reader);
                }
            }

            public async Task PutObjectAsync<T>(string entityName, string objectId, T contentObject)
            {
                using (var response = await _httpClient.PutAsJsonAsync(GetEntityUri(entityName, objectId), contentObject))
                {
                    await ProcessResponseAsync(response, "Failed to PUT object.");
                }
            }

            public async Task MergeJObjectAsync(string entityName, string objectId, JObject contentObject)
            {
                using (var contentStream = new MemoryStream())
                {
                    var writer = new JsonTextWriter(new StreamWriter(contentStream));
                    contentObject.WriteTo(writer);
                    writer.Flush();
                    contentStream.Seek(0, SeekOrigin.Begin);

                    using (var request = new HttpRequestMessage(new HttpMethod("MERGE"), GetEntityUri(entityName, objectId)))
                    {
                        request.Content = new StreamContent(contentStream);
                        request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");  
                        using (var response = await this._httpClient.SendAsync(request))
                        {
                            await ProcessResponseAsync(response, "Failed to MERGE object");
                        }
                    }
                }
            }

            private async Task ProcessResponseAsync(HttpResponseMessage response, string failureMessage)
            {
                if (!response.IsSuccessStatusCode)
                {
                    throw CreateGraphException(failureMessage, JObject.Load(new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync()))));
                }
            }

            private Exception CreateGraphException(string message, JObject errorResponse)
            {
                var errorDetails = errorResponse["odata.error"] as JObject;
                if (errorDetails != null)
                {
                    return new GraphApiException(message, errorDetails.Value<string>("code"), errorDetails["message"].Value<string>("value"));
                }
                return new GraphApiException(message, "Unknown", "Unknown error");
            }
        }
    }
}